iT邦幫忙

2023 iThome 鐵人賽

DAY 12
0
自我挑戰組

Concurrency in go 讀書心得系列 第 12

12.Preventing goroutine leaks

  • 分享至 

  • xImage
  •  

goroutines占用資源較少且易於創建。執行時將多個 goroutine復用到任意數量的作業系統執行緒,以便我們不必擔心抽象層級。但是他們會花費成本資源,並且goroutine不會被runtime GC,所以無論記憶體占用多少,我們都應該確保他們被清理乾淨

goroutines代表可能平行(parallel)或不平行運行的工作單位。該goroutine有幾條路徑終止:

1.當它完成任務。
2.當它遇到不可恢復的錯誤無法繼續它的任務。
3.當它被告知停止當前任務。
前兩條我們已經知曉,可以通過算法實現。但如何取消當前任務?
由於網絡效應,這最重要的一點是:如果你已經開始了一個goroutine,那麼它很可能以某種有組織的方式與其他幾個goroutines合作。我們甚至可以把這種相互連接表現為一張圖表,這時該goroutine能否停下來還取決於處在互動的其他 goroutines。我們將在下一章中繼續關注大規模並發產生的相互依賴關係,但現在讓我們考慮如何確保單個goroutine得到清理。讓我們從一個簡單的goroutine洩漏開始:

package main

import (
	"fmt"
)

func main() {
	doWork := func(strings <-chan string) <-chan interface{} {
		completed := make(chan interface{})
		go func() {
			defer fmt.Println("doWork exited.")
			defer close(completed)
			for s := range strings {
				// Do something interesting
				fmt.Println(s)
			}
		}()
		return completed
	}

	doWork(nil)
	// Perhaps more work is done here
	fmt.Println("Done.")
}

這行代碼調用了doWork函數,但傳遞了nil作為strings通道。這意味著,內部的goroutine的迴圈將不會接收到任何數據,因為對一個nil通道的接收操作永遠都不會返回。
當您運行這個程序時,它將僅僅輸出 "Done."。這是因為doWork內部的goroutine無法從nil通道接收到任何數據,且strings通道從未被關閉(因為它是nil),所以"doWork exited."也不會被輸出。

strings通道永遠無法讀取到內容(因為它是nil),而且包含 doWork的goroutine將在這個過程的整個生命周期中保留在內存中(如果我們在doWork和主goutoutine中加入了goroutine,我們甚至會死鎖)。
在這個例子中,整個進程的生命周期很短,但是在一個真正的程序中,goroutines可以很容易地在一個長期生命的程序開始時啟動,導致內存利用率下降。


解決這種情況的方法是建立一個信號,按照慣例,這個信號通常是一個名為done的只讀通道。父例程將該通道傳遞給子例程,然後在想要取消子例程時關閉該通道。

package main

import (
	"fmt"
	"time"
)

func main() {

    // doWork函數現在接受一個新的參數:done通道。當這個通道被關閉時,內部的goroutine應當停止其工作並退出。
	doWork := func(
		done <-chan interface{},
		strings <-chan string,
	) <-chan interface{} { // <1>
		terminated := make(chan interface{})
		go func() {
			defer fmt.Println("doWork exited.")
			defer close(terminated)
            // 在goroutine中,select語句用於同時等待多個通道操作。如果strings通道接收到新的字符串,它將處理和輸出該字符串。如果done通道被關閉,那麼goroutine將結束。
			for {
				select {
				case s := <-strings:
					// Do something interesting
					fmt.Println(s)
				case <-done: // <2>
					return
				}
			}
		}()
		return terminated
	}

	done := make(chan interface{})
	terminated := doWork(done, nil)
    
    // 在主函式main中,啟動了另一個goroutine來在1秒後關閉done通道。這表示doWork中的goroutine將在大約1秒後收到終止信號並退出。
	go func() { // <3>
		// Cancel the operation after 1 second.
		time.Sleep(1 * time.Second)
		fmt.Println("Canceling doWork goroutine...")
		close(done)
	}()
    
    // 主函式main會阻塞,直到doWork中的goroutine完全終止。這確保了在程序的main函式結束前,所有背景工作都已經完成。
	<-terminated // <4>
	fmt.Println("Done.")
}

這個改善的版本提供了一種方法來優雅地終止和清理goroutines,防止了潛在的goroutine泄露。它通過done通道提供了一個外部信號來告訴doWork中的goroutine何時該停止工作。這種模式在Go的併發設計中是很常見的,因為它允許更好地控制和管理goroutines。


上一篇
11.For-Select-Loop
下一篇
13.Or-channel
系列文
Concurrency in go 讀書心得30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言